package onfido

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"strings"
	"time"

	"code.bydev.io/cht/customer/kyc-stub.git/pkg/bybit/kyc/common/enums"
	byoneconfig "code.bydev.io/cht/customer/kyc_admin_service/internal/config"
	"code.bydev.io/cht/customer/kyc_admin_service/internal/pkg/config"
	"code.bydev.io/cht/customer/kyc_admin_service/internal/pkg/constdata"
	"code.bydev.io/cht/customer/kyc_admin_service/internal/pkg/helper"
	"code.bydev.io/cht/customer/kyc_admin_service/internal/pkg/imgcompress"
	kae_dto "code.bydev.io/cht/customer/kyc_admin_service/internal/pkg/kycadminevent/dto"
	"code.bydev.io/cht/customer/kyc_admin_service/internal/pkg/kycprovider/dto"
	sdkdto "code.bydev.io/cht/customer/kyc_admin_service/internal/pkg/kycprovider/provider"
	"code.bydev.io/cht/customer/kyc_admin_service/internal/pkg/teller"
	"code.bydev.io/cht/customer/kyc_admin_service/internal/pkg/utils/timeutil"
	"code.bydev.io/cht/customer/kyc_admin_service/internal/utils"
	"code.bydev.io/frameworks/byone/core/logc"
	"code.bydev.io/frameworks/byone/core/logx"
	"code.bydev.io/frameworks/byone/rest/httpc"
	"emperror.dev/errors"
	"git.bybit.com/svc/go/pkg/bzap"
	"git.bybit.com/svc/mod/pkg/bsecret/bsecretsecret"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/service/s3/s3manager"
	"go.uber.org/zap"
)

const ErrTypeResourceNotFound = "resource_not_found"

var ErrResourceNotFound = errors.New(ErrTypeResourceNotFound)

const (
	PathRetrieveApplicant   string = "/applicants/%s"
	PathCreateApplicants    string = "/applicants/"
	PathUpdateApplicants    string = "/applicants/%s"
	PathCreateWorkflowRuns  string = "/workflow_runs/"
	PathRetrieveWorkflowRun string = "/workflow_runs/%s"
	PathListTasks           string = "/workflow_runs/%s/tasks"
	PathRetrieveTask        string = "/workflow_runs/%s/tasks/%s"
	PathRetrieveDocument    string = "/documents/%s"
	PathDownloadDocument    string = "/documents/%s/download"
	PathRetrieveLivePhoto   string = "/live_photos/%s"
	PathDownloadLivePhoto   string = "/live_photos/%s/download"
	PathSDKToken            string = "/sdk_token"
	PathPing                string = "/ping"
	PathListDocuments       string = "/documents?applicant_id=%s"
	PathListLivePhotos      string = "/live_photos?applicant_id=%s"
	retryLimit              int    = 1
)

type OnfidoProvider struct {
	conf   *byoneconfig.OnfidoConfig
	logger *zap.Logger
	teller sdkdto.Teller
	imgCps imgcompress.ImgCps
}

func NewOnfidoProvider(onfidoConfig *byoneconfig.OnfidoConfig, logger *zap.Logger, tell sdkdto.Teller) *OnfidoProvider {
	op := new(OnfidoProvider)
	op.conf = onfidoConfig
	op.logger = logger
	op.teller = tell
	op.imgCps = imgcompress.New(logger, tell)

	return op
}

// GetAPIURL 获取api接口
func (s *OnfidoProvider) GetAPIURL(ctx context.Context) string {
	return fmt.Sprintf("%s/%s", s.conf.Domain, s.conf.APIVersion)
}

func (s *OnfidoProvider) GetUserID(memberID int64, provider dto.KYCProvider) (string, error) {
	userID, err := sdkdto.HashUserID(int(memberID), provider)
	if err != nil {
		return "", err
	}
	return userID, nil
}

type CreateApplicantRequest struct {
	FirstName string `json:"first_name"`
	LastName  string `json:"last_name"`
}

var (
	defaultTimeout = 3 * time.Second
)

// CreateApplicant Create an Onfido applicant
func (s *OnfidoProvider) CreateApplicant(ctx context.Context, memberID int64, provider dto.KYCProvider) (*OnfidoApplicant, error) {
	var (
		respBody []byte
		httpCode int
		err      error
	)
	resp := new(OnfidoApplicant)
	logger := s.logger.With(zap.String("path", PathCreateApplicants))

	externalUID, err := s.GetUserID(memberID, provider)
	if err != nil {
		return nil, err
	}
	reqBody, err := json.Marshal(CreateApplicantRequest{
		FirstName: externalUID,
		LastName:  "Bybit.com",
	})
	if err != nil {
		return nil, err
	}
	i := 0
	for {
		httpCode, respBody, err = s.request(ctx, PathCreateApplicants, http.MethodPost, reqBody, defaultTimeout)
		if i >= retryLimit || err == nil {
			break
		}
		i++
	}
	if err != nil {
		bzap.LogError(logger, "OnfidoProvider CreateApplicant error", err)
		_ = s.teller.KycAlarm(ctx, "OnfidoProvider CreateApplicant error", err)
		return nil, err
	}
	if apiErr := s.apiErrorHandler(ctx, PathCreateApplicants, httpCode, respBody); apiErr != nil {
		return nil, apiErr
	}
	logger.Info("OnfidoProvider CreateApplicant s.request info", zap.String("respBody", string(respBody)))
	if err = json.Unmarshal(respBody, resp); err != nil {
		bzap.LogError(logger, "OnfidoProvider CreateApplicant json.Unmarshal error", err)
		_ = s.teller.KycAlarm(ctx, "OnfidoProvider CreateApplicant json.Unmarshal error", err)
		return nil, err
	}
	s.logger.Info("OnfidoProvider CreateApplicant get resp", zap.Reflect("resp", resp))

	return resp, nil
}

type WebSDKTokenRequest struct {
	ApplicantID    string `json:"applicant_id"`
	Referer        string `json:"referer"`
	CrossDeviceURL string `json:"cross_device_url,omitempty"`
}

type AppSDKTokenRequest struct {
	ApplicantID   string `json:"applicant_id"`
	ApplicationID string `json:"application_id"`
}

func (s *OnfidoProvider) GetToken(ctx context.Context, hashID string, ttl int, level dto.KYCLevel) (string, error) {
	return "", nil
}

func (s *OnfidoProvider) GetApplicant(ctx context.Context, applicantID string) (interface{}, error) {
	var (
		respBody []byte
		httpCode int
		err      error
	)
	path := fmt.Sprintf(PathRetrieveApplicant, applicantID)
	resp := new(OnfidoApplicant)
	logger := s.logger.With(zap.String("path", path))

	httpCode, respBody, err = s.request(ctx, path, http.MethodGet, nil, defaultTimeout)
	if err != nil {
		bzap.LogError(logger, "OnfidoProvider RetrieveApplicant error", err)
		_ = s.teller.KycAlarm(ctx, "OnfidoProvider RetrieveApplicant error", err)
		return nil, err
	}
	if apiErr := s.apiErrorHandler(ctx, path, httpCode, respBody); apiErr != nil {
		return nil, apiErr
	}
	logger.Info("OnfidoProvider RetrieveApplicant s.request info", zap.String("respBody", string(respBody)))
	if err = json.Unmarshal(respBody, resp); err != nil {
		bzap.LogError(logger, "OnfidoProvider RetrieveApplicant json.Unmarshal error", err)
		_ = s.teller.KycAlarm(ctx, "OnfidoProvider RetrieveApplicant json.Unmarshal error", err)
		return nil, err
	}
	s.logger.Info("OnfidoProvider RetrieveApplicant get resp", zap.Reflect("resp", resp))

	return resp, nil
}

func (s *OnfidoProvider) UpdateApplicantAddress(ctx context.Context, applicantID string, adr *UpdateApplicantAddress) (interface{}, error) {
	var (
		respBody []byte
		httpCode int
		err      error
	)
	path := fmt.Sprintf(PathUpdateApplicants, applicantID)
	resp := new(OnfidoApplicant)
	logger := s.logger.With(zap.String("path", path))
	reqBody, err := json.Marshal(adr)
	if err != nil {
		bzap.LogError(logger, "onfido API: UpdateApplicant error", err)
		_ = s.teller.KycAlarm(ctx, "onfido API: UpdateApplicant error", err)
	}

	httpCode, respBody, err = s.request(ctx, path, http.MethodPut, reqBody, defaultTimeout)
	if err != nil {
		bzap.LogError(logger, "onfido API: UpdateApplicant error", err)
		_ = s.teller.KycAlarm(ctx, "onfido API: UpdateApplicant error", err)
		return nil, err
	}
	if apiErr := s.apiErrorHandler(ctx, path, httpCode, respBody); apiErr != nil {
		return nil, apiErr
	}
	logger.Info("onfido API: UpdateApplicant s.request info", zap.String("respBody", string(respBody)))
	if err = json.Unmarshal(respBody, resp); err != nil {
		bzap.LogError(logger, "onfido API: UpdateApplicant json.Unmarshal error", err)
		_ = s.teller.KycAlarm(ctx, "onfido API: UpdateApplicant json.Unmarshal error", err)
		return nil, err
	}
	s.logger.Info("onfido API: UpdateApplicant get resp", zap.Reflect("resp", resp))

	return resp, nil
}

type CreateWorkflowRunRequest struct {
	ApplicantID string `json:"applicant_id"`
	WorkflowID  string `json:"workflow_id"`
}

func (s *OnfidoProvider) CreateWorkflowRun(ctx context.Context, applicantID string, level dto.KYCLevel, docType enums.KYCDocType) (*OnfidoWorkflowRun, error) {
	var wfID string
	if level == dto.Level2 {
		wfID = s.conf.Level2WorkflowID
	} else {
		if docType == enums.KYCDocType_KYC_DOC_TYPE_PASSPORT {
			wfID = s.conf.Level1WorkflowIDOneSide
		} else {
			wfID = s.conf.Level1WorkflowIDTwoSide
		}
	}
	return s.createWorkflowRun(ctx, applicantID, wfID)
}

func (s *OnfidoProvider) createWorkflowRun(ctx context.Context, applicantID, workflowID string) (*OnfidoWorkflowRun, error) {
	var (
		respBody []byte
		httpCode int
		err      error
	)
	resp := new(OnfidoWorkflowRun)
	logger := s.logger.With(zap.String("path", PathCreateWorkflowRuns))
	reqBody, err := json.Marshal(CreateWorkflowRunRequest{
		ApplicantID: applicantID,
		WorkflowID:  workflowID,
	})
	if err != nil {
		return nil, err
	}
	httpCode, respBody, err = s.request(ctx, PathCreateWorkflowRuns, http.MethodPost, reqBody, defaultTimeout)
	if err != nil {
		bzap.LogError(logger, "OnfidoProvider CreateWorkflowRun error", err)
		_ = s.teller.KycAlarm(ctx, "OnfidoProvider CreateWorkflowRun error", err)
		return nil, err
	}
	if apiErr := s.apiErrorHandler(ctx, PathCreateWorkflowRuns, httpCode, respBody); apiErr != nil {
		return nil, apiErr
	}
	logger.Info("OnfidoProvider CreateWorkflowRun s.request info", zap.String("respBody", string(respBody)))
	if err = json.Unmarshal(respBody, resp); err != nil {
		bzap.LogError(logger, "OnfidoProvider CreateWorkflowRun json.Unmarshal error", err)
		_ = s.teller.KycAlarm(ctx, "OnfidoProvider CreateWorkflowRun json.Unmarshal error", err)
		return nil, err
	}
	s.logger.Info("OnfidoProvider CreateWorkflowRun get resp", zap.Reflect("resp", resp))

	return resp, nil
}

// RetrieveWorkflowRun Retrieve workflow run from onfido
// https://documentation.onfido.com/#retrieve-workflow-run
func (s *OnfidoProvider) RetrieveWorkflowRun(ctx context.Context, workflowRunID string) (*OnfidoWorkflowRun, error) {
	var (
		respBody []byte
		httpCode int
		err      error
	)
	path := fmt.Sprintf(PathRetrieveWorkflowRun, workflowRunID)
	resp := new(OnfidoWorkflowRun)
	reqID := helper.ReqID(ctx)
	logger := s.logger.With(zap.String("reqID", reqID), zap.String("path", path))
	httpCode, respBody, err = s.request(ctx, path, http.MethodGet, nil, defaultTimeout)
	if err != nil {
		bzap.LogError(logger, "onfido API: retrieve workflow run error", err)
		_ = s.teller.KycAlarm(ctx, "onfido API: retrieve workflow run error", err)
		return nil, err
	}
	if apiErr := s.apiErrorHandler(ctx, path, httpCode, respBody); apiErr != nil {
		return nil, apiErr
	}
	logger.Info("onfido API: retrieve workflow run response", zap.String("respBody", string(respBody)))
	if err = json.Unmarshal(respBody, resp); err != nil {
		bzap.LogError(logger, "onfido API: retrieve workflow run json unmarshal error", err)
		_ = s.teller.KycAlarm(ctx, "onfido API: retrieve workflow run json unmarshal error", err)
		return nil, err
	}
	resp.Level = resp.SetKYCLevel(s.conf)
	s.logger.Info("onfido API: retrieve workflow run success", zap.Reflect("resp", resp))

	return resp, nil
}

func (s *OnfidoProvider) GetRejectReasons(ctx context.Context, wfr *OnfidoWorkflowRun) ([]string, *AddressParsed, error) {
	var (
		err    error
		taskID string
	)
	adr := new(AddressParsed)
	reqID := helper.ReqID(ctx)
	ID := wfr.GetWorkflowRunID()
	logger := s.logger.With(zap.String("reqID", reqID), zap.String("workflowID", ID))
	// List Tasks
	tasks, err := s.ListWorkflowRunTasks(ctx, ID)
	if err != nil {
		bzap.LogError(logger, "onfido API: list workflow run tasks error", err)
		return nil, adr, err
	}
	// Retrieve Task
	if wfr.Level == dto.Level1 || wfr.Level == dto.Level1Secondary {
		for _, task := range *tasks {
			if task.TaskDefID.IsLevel1Check() {
				taskID = task.ID
			}
		}
	} else {
		for _, task := range *tasks {
			if task.TaskDefID.IsLevel2Check() {
				taskID = task.ID
			}
		}
	}
	logger = logger.With(zap.String("taskID", taskID))
	taskDetail, err := s.RetrieveWorkflowRunTask(ctx, ID, taskID)
	if err != nil {
		bzap.LogError(logger, "onfido API: retrieve workflow run task detail error", err)
		return nil, adr, err
	}
	if wfr.Level == dto.Level1 || wfr.Level == dto.Level1Secondary {
		lv1Output := &Level1TaskOutput{}
		lv1Detail, err := json.Marshal(taskDetail.Output)
		if err != nil {
			bzap.LogError(logger, "onfido API: retrieve workflow run task json marshal error", err)
			return nil, adr, err
		}
		err = json.Unmarshal(lv1Detail, lv1Output)
		if err != nil {
			bzap.LogError(logger, "onfido API: retrieve workflow run task json unmarshal error", err)
			return nil, adr, err
		}
		return lv1Output.GetRejectReasons(), adr, nil
	} else {
		lv2Output := &Level2TaskOutput{}
		lv2Detail, err := json.Marshal(taskDetail.Output)
		if err != nil {
			bzap.LogError(logger, "onfido API: retrieve workflow run task json marshal error", err)
			return nil, adr, err
		}
		err = json.Unmarshal(lv2Detail, lv2Output)
		if err != nil {
			bzap.LogError(logger, "onfido API: retrieve workflow run task json unmarshal error", err)
			return nil, adr, err
		}
		adr.Country = lv2Output.Properties.AddressParsed.Country
		adr.State = lv2Output.Properties.AddressParsed.State
		adr.Postcode = lv2Output.Properties.AddressParsed.Postcode
		adr.Town = lv2Output.Properties.AddressParsed.Town
		adr.IssuingCountry = lv2Output.Properties.IssuingCountry
		return lv2Output.GetRejectReasons(), adr, nil
	}
}

func (s *OnfidoProvider) ListWorkflowRunTasks(ctx context.Context, workflowRunID string) (*WorkflowRunTasks, error) {
	var (
		respBody []byte
		httpCode int
		err      error
	)
	path := fmt.Sprintf(PathListTasks, workflowRunID)
	resp := new(WorkflowRunTasks)
	reqID := helper.ReqID(ctx)
	logger := s.logger.With(zap.String("reqID", reqID), zap.String("path", path))
	httpCode, respBody, err = s.request(ctx, path, http.MethodGet, nil, defaultTimeout)
	if err != nil {
		bzap.LogError(logger, "onfido API: list workflow run tasks error", err)
		_ = s.teller.KycAlarm(ctx, "onfido API: list workflow run tasks error", err)
		return nil, err
	}
	if apiErr := s.apiErrorHandler(ctx, path, httpCode, respBody); apiErr != nil {
		return nil, apiErr
	}
	logger.Info("onfido API: list workflow run tasks response", zap.String("respBody", string(respBody)))
	if err = json.Unmarshal(respBody, resp); err != nil {
		bzap.LogError(logger, "onfido API: list workflow run tasks json unmarshal error", err)
		_ = s.teller.KycAlarm(ctx, "onfido API: list workflow run tasks json unmarshal error", err)
		return nil, err
	}
	s.logger.Info("onfido API: list workflow run tasks success", zap.Reflect("resp", resp))

	return resp, nil
}

func (s *OnfidoProvider) RetrieveWorkflowRunTask(ctx context.Context, workflowRunID, taskID string) (*WorkflowRunTaskDetail, error) {
	var (
		respBody []byte
		httpCode int
		err      error
	)
	path := fmt.Sprintf(PathRetrieveTask, workflowRunID, taskID)
	resp := new(WorkflowRunTaskDetail)
	reqID := helper.ReqID(ctx)
	logger := s.logger.With(zap.String("reqID", reqID), zap.String("path", path))
	httpCode, respBody, err = s.request(ctx, path, http.MethodGet, nil, defaultTimeout)
	if err != nil {
		bzap.LogError(logger, "onfido API: retrieve workflow run task detail error", err)
		_ = s.teller.KycAlarm(ctx, "onfido API: retrieve workflow run task detail error", err)
		return nil, err
	}
	if apiErr := s.apiErrorHandler(ctx, path, httpCode, respBody); apiErr != nil {
		return nil, apiErr
	}
	logger.Info("onfido API: retrieve workflow run task detail response", zap.String("respBody", string(respBody)))
	if err = json.Unmarshal(respBody, resp); err != nil {
		bzap.LogError(logger, "onfido API: retrieve workflow run task detail json unmarshal error", err)
		_ = s.teller.KycAlarm(ctx, "onfido API: retrieve workflow run task detail json unmarshal error", err)
		return nil, err
	}
	s.logger.Info("onfido API: retrieve workflow run task detail success", zap.Reflect("resp", resp))

	return resp, nil
}

func (s *OnfidoProvider) GetDocuments(ctx context.Context, applicantID, inspectionID string, ch chan<- *dto.Document) error {
	return nil
}

func (s *OnfidoProvider) ResetApplicant(applicantID string) (interface{}, error) {
	return "", nil
}

// NotifyFaceAuth The face auth callback notify
// NOTE: The face auth of Onfido Not Supported
func (s *OnfidoProvider) NotifyFaceAuth(ctx context.Context, data []byte) (interface{}, error) {
	return "", nil
}

func (s *OnfidoProvider) request(ctx context.Context, path, method string, body []byte, t time.Duration) (int, []byte, error) {
	logc.Infow(ctx, "request begin")
	headerMap := map[string]string{
		"Accept":        "application/json",
		"Content-Type":  "application/json",
		"Connection":    "keep-alive",
		"Authorization": "Token token=" + s.conf.APIToken,
	}
	url := s.GetAPIURL(ctx) + path
	// byone框架
	httpClient := &http.Client{
		Timeout: t,
	}
	httpService := httpc.NewServiceWithClient("httpService", httpClient, func(r *http.Request) *http.Request {
		for k, v := range headerMap {
			r.Header.Set(k, v)
		}
		return r
	})
	// 重新构造请求
	httpReq, err := http.NewRequest(method, url, bytes.NewBuffer(body))
	if err != nil {
		return 0, nil, err
	}
	response, err := httpService.DoRequest(httpReq)
	if err != nil {
		_ = teller.GlobTeller(ctx, "httpReq:"+url+" error:", err)
		logc.Errorw(ctx, "httpService.Do error", logc.Field("err", err))
		return 0, nil, err
	}
	defer func() {
		if err = response.Body.Close(); err != nil {
			logc.Errorw(ctx, "HttpC response.Body.Close error", logc.Field("err", err))
		}
	}()
	logc.Infow(ctx, "HttpC client.Do success", logc.Field("status", response.Status))
	respBody, err := ioutil.ReadAll(response.Body)
	if err != nil {
		logc.Errorw(ctx, "HttpC ioutil.ReadAll error", logc.Field("err", err))
		return 0, nil, err
	}
	if response.StatusCode != http.StatusOK {
		logc.Errorw(ctx, "client.Do respCode not success", logc.Field("respBody", string(respBody)))
		return response.StatusCode, respBody, nil
	}
	return response.StatusCode, respBody, nil
}

func (s *OnfidoProvider) apiErrorHandler(ctx context.Context, path string, httpCode int, respBody []byte) error {
	var err error
	reqID := helper.ReqID(ctx)
	logger := s.logger.With(zap.String("reqID", reqID), zap.String("path", path))
	if httpCode >= http.StatusOK && httpCode <= http.StatusIMUsed {
		return nil
	}
	apiErr := new(APIError)
	err = json.Unmarshal(respBody, apiErr)
	if err != nil {
		bzap.LogError(logger.With(zap.String("respBody", string(respBody))), "onfido API: json unmarshal response body error", err)
		_ = s.teller.KycAlarm(ctx, "onfido API: json unmarshal response body error", err)

		return err
	}
	if apiErr.Error.Type == ErrTypeResourceNotFound {
		return ErrResourceNotFound
	}
	return errors.New(apiErr.Error.Type)
}

func (s *OnfidoProvider) GetKYCAdminDetail(ctx context.Context, idempotentID string, ntf *kae_dto.KYCEventNotify, firstPoi *dto.Identify) (*dto.KYCAdminDetail, []*dto.KYCActionImage, error) {
	var (
		err                                                            error
		country, frontImgBucketStr, backImgBucketStr, faceImgBucketStr string
	)

	workflowRunID := ntf.RecordID
	logger := s.logger.With(
		zap.String("reqID", helper.ReqID(ctx)),
		zap.String("idempotentID", idempotentID),
		zap.Int64("memberID", ntf.MemberID),
		zap.String("workflowRunID", workflowRunID))

	detail, err := s.RetrieveWorkflowRun(ctx, workflowRunID)
	if err != nil {
		bzap.LogError(logger, "onfido state: retrieve workflow run error", err)
		_ = s.teller.KycAlarm(ctx, "onfido state: retrieve workflow run(ID:"+workflowRunID+") error", err)
		return nil, nil, err
	}
	logger.Info("onfido state: convert to interface")
	output, err := OnfidoWorkflowOutputCov(detail.Output, detail.Level)
	if err != nil {
		bzap.LogError(logger.With(zap.Any("output", detail.Output)), "onfido state: ensureWorkflowOutput error", err)
		return nil, nil, err
	}

	providerDecision := detail.GetReviewResult()
	providerRejectReason, adr, errRea := s.GetRejectReasons(ctx, detail)
	if errRea != nil {
		logc.Errorw(ctx, "onfido state: get reject reason error", logc.Field("err", errRea))
		return nil, nil, errRea
	}
	providerRejectReasonStr := ""
	if len(providerRejectReason) >= 1 {
		providerRejectReasonStr = strings.Join(providerRejectReason, ",")
	}
	docNumber := output.GetDocNumber()
	addressCount := ""
	if ntf.Level == int(dto.Level1) {
		country = output.GetCountry()
	} else {
		country = adr.IssuingCountry
		addressCount = adr.Country
	}
	actionImgs, _ := s.GetActionImages(ctx, ntf.MemberID, ntf.Level, ntf.AccountID, idempotentID, workflowRunID, output)

	adminDetail := &dto.KYCAdminDetail{
		ProviderDecision:     int8(providerDecision),
		ProviderRejectReason: providerRejectReasonStr,
		DocType:              int8(output.GetDocType()),
		DocSubtype:           int8(output.GetSubDocType()),
		DocNumber:            docNumber,
		Country:              country,
		Nationality:          output.GetNationality(),
		AddressCountry:       addressCount,
		FirstName:            output.GetFirstName(),
		LastName:             output.GetLastName(),
		Dob:                  output.GetDOB(),
		DocDetail: dto.KYCAdminDocDetail{
			DocFront:       frontImgBucketStr,
			DocBack:        backImgBucketStr,
			PersonalNumber: output.GetIDCard(),
		},
		IssuingDate: output.GetIssuingDate(),
		ExpireDate:  output.GetExpiryDate(),
		FaceDetail: dto.KYCAdminFaceDetail{
			FaceImage:         faceImgBucketStr,
			LivenessImage:     "",
			SimilarityChecked: "",
			ValidityChecked:   "",
		},
		AddressDetail: dto.KYCAdminAddressDetail{
			Country:     output.GetAddressParsed().Country,
			State:       output.GetAddressParsed().State,
			City:        output.GetAddressParsed().Town,
			FullAddress: output.GetAddress(),
			PostalCode:  output.GetAddressParsed().Postcode,
		},
		ClientDetail: dto.KYCAdminClientDetail{
			ClientIP: "",
			From:     "",
			Source:   "",
		},
	}

	return adminDetail, actionImgs, nil
}

func (s *OnfidoProvider) GetAllUploadImagesInfo(ctx context.Context, idempotentID, inspectionID string, output WorkflowOutput) (map[string]*dto.Document, error) {
	ch := make(chan *dto.Document, 5)
	imgIDs := make(map[string]string)
	logger := s.logger.With(zap.String("reqID", helper.ReqID(ctx)), zap.String("idempotentID", idempotentID),
		zap.String("inspectionID", inspectionID))
	if len(output.GetDocIDs()) > 0 {
		for _, did := range output.GetDocIDs() {
			docInfo, err := s.RetrieveDocument(ctx, did.ID)
			if err != nil {
				logger.Warn("onfido statement: retrieve document(ID:" + did.ID + ") info failed")
			} else {
				if docInfo.Side == "front" {
					imgIDs["front"] = did.ID
				} else {
					imgIDs["back"] = did.ID
				}
			}
		}
	}
	if len(output.GetLivePhotoIDs()) > 0 {
		imgIDs["live_photo"] = output.GetLivePhotoIDs()[0].ID
	}
	if len(imgIDs) < 1 {
		return nil, nil
	}
	s.logger.Info("onfido statement: images", zap.Any("imgIDs", imgIDs))
	go func() {
		e := s.GetAllDocuments(ctx, inspectionID, imgIDs, ch)
		if e != nil {
			bzap.LogError(s.logger, "provider.GetDocuments error", e)
		}
	}()

	if docs, e := uploadDocument(ctx, s.logger, idempotentID, ch); e != nil {
		return nil, e
	} else {
		return docs, nil
	}
}

func (s *OnfidoProvider) GetActionImages(ctx context.Context, memberID int64, level int, applicantID, idempotentID, inspectionID string, output WorkflowOutput) ([]*dto.KYCActionImage, error) {
	actionImgs := make([]*dto.KYCActionImage, 0)
	logger := s.logger.With(zap.String("reqID", helper.ReqID(ctx)), zap.String("idempotentID", idempotentID), zap.String("inspectionID", inspectionID))
	if len(output.GetDocIDs()) > 0 {
		for _, did := range output.GetDocIDs() {
			docInfo, err := s.RetrieveDocument(ctx, did.ID)
			if err != nil {
				logger.Warn("onfido statement: retrieve document(ID:" + did.ID + ") info failed")
				return nil, err
			}
			// POA 有时会没有 side 字段
			if docInfo.Side == "front" || docInfo.Side == "" {
				actionImgs = append(actionImgs, &dto.KYCActionImage{
					IdempotentID: idempotentID,
					MemberID:     memberID,
					Provider:     enums.KYCProvider_PROVIDER_ONFIDO,
					AccountID:    applicantID,
					RecordID:     inspectionID,
					ImgID:        did.ID,
					ImgType:      dto.ImgTypeDocFront,
				})
			} else {
				actionImgs = append(actionImgs, &dto.KYCActionImage{
					IdempotentID: idempotentID,
					MemberID:     memberID,
					Provider:     enums.KYCProvider_PROVIDER_ONFIDO,
					AccountID:    applicantID,
					RecordID:     inspectionID,
					ImgID:        did.ID,
					ImgType:      dto.ImgTypeDocBack,
				})
			}
		}
	}
	if len(output.GetLivePhotoIDs()) > 0 {
		actionImgs = append(actionImgs, &dto.KYCActionImage{
			IdempotentID: idempotentID,
			MemberID:     memberID,
			Provider:     enums.KYCProvider_PROVIDER_ONFIDO,
			AccountID:    applicantID,
			RecordID:     inspectionID,
			ImgID:        output.GetLivePhotoIDs()[0].ID,
			ImgType:      dto.ImgTypeFace,
		})
	}
	s.logger.Info("onfido statement: images", zap.Any("actionImgs", actionImgs))
	return actionImgs, nil
}

func (s *OnfidoProvider) GetAllDocuments(ctx context.Context, inspectionID string, imgIDs map[string]string, ch chan<- *dto.Document) error {
	var (
		err error
		doc *dto.Document
	)
	defer close(ch)
	defer func() {
		if err != nil {
			_ = s.teller.KycAlarm(ctx, "onfido statement: get document failed", err)
		}
	}()

	for tp, id := range imgIDs {
		f := helper.TimeCost(s.logger, "onfido statement: download image type:"+tp)
		if tp == "live_photo" {
			doc, err = s.getLivePhoto(ctx, inspectionID, id)
		} else {
			doc, err = s.getDocument(ctx, inspectionID, id)
		}
		f()
		if err != nil {
			s.logger.Error("onfido statement: download image failed type:"+tp, zap.Error(err))
			return err
		}
		switch tp {
		case "front":
			doc.Tag = dto.ImageTagFront
		case "back":
			doc.Tag = dto.ImageTagBack
		case "live_photo":
			doc.Tag = dto.ImageTagFace
		}
		s.logger.Info(fmt.Sprintf("File type:%s size:%d", tp, len(doc.Data)))
		doc.Type = "id_card"

		select {
		case <-ctx.Done():
			return ctx.Err()
		case ch <- doc:
		}
	}

	return nil
}

func (s *OnfidoProvider) RetrieveDocument(ctx context.Context, docID string) (*DocumentInfo, error) {
	var (
		respBody []byte
		httpCode int
		err      error
	)
	path := fmt.Sprintf(PathRetrieveDocument, docID)
	resp := new(DocumentInfo)
	reqID := helper.ReqID(ctx)
	logger := s.logger.With(zap.String("reqID", reqID), zap.String("path", path))
	httpCode, respBody, err = s.request(ctx, path, http.MethodGet, nil, defaultTimeout)
	if err != nil {
		bzap.LogError(logger, "onfido API: retrieve document info error", err)
		_ = s.teller.KycAlarm(ctx, "onfido API: retrieve document info error", err)
		return nil, err
	}
	if apiErr := s.apiErrorHandler(ctx, path, httpCode, respBody); apiErr != nil {
		return nil, apiErr
	}
	logger.Info("onfido API: retrieve document info response", zap.String("respBody", string(respBody)))
	if err = json.Unmarshal(respBody, resp); err != nil {
		bzap.LogError(logger, "onfido API: retrieve document info json unmarshal error", err)
		_ = s.teller.KycAlarm(ctx, "onfido API: retrieve document info json unmarshal error", err)
		return nil, err
	}
	s.logger.Info("onfido API: retrieve document info success", zap.Reflect("resp", resp))

	return resp, nil
}

func (s *OnfidoProvider) DownloadDocument(ctx context.Context, docID string) ([]byte, error) {
	var (
		err      error
		httpCode int
		respBody []byte
	)
	path := fmt.Sprintf(PathDownloadDocument, docID)
	resp := new(DocumentInfo)
	reqID := helper.ReqID(ctx)
	logger := s.logger.With(zap.String("reqID", reqID), zap.String("path", path))
	httpCode, respBody, err = s.request(ctx, path, http.MethodGet, nil, config.GetDownloadImageTimeout())
	if err != nil {
		bzap.LogError(logger, "onfido API: download document error", err)
		_ = s.teller.KycAlarm(ctx, "onfido API: download document error", err)
		return nil, err
	}
	if httpCode == http.StatusNotFound {
		s.logger.Warn("onfido API: download document not found", zap.String("docID", docID))
		return nil, nil
	}
	if apiErr := s.apiErrorHandler(ctx, path, httpCode, respBody); apiErr != nil {
		return nil, apiErr
	}
	s.logger.Info("onfido API: download document success", zap.Reflect("resp", resp))

	return respBody, nil
}

func (s *OnfidoProvider) RetrieveLivePhotoInfo(ctx context.Context, photoID string) (*LivePhotoInfo, error) {
	var (
		respBody []byte
		httpCode int
		err      error
	)
	path := fmt.Sprintf(PathRetrieveLivePhoto, photoID)
	resp := new(LivePhotoInfo)
	reqID := helper.ReqID(ctx)
	logger := s.logger.With(zap.String("reqID", reqID), zap.String("path", path))
	httpCode, respBody, err = s.request(ctx, path, http.MethodGet, nil, defaultTimeout)
	if err != nil {
		bzap.LogError(logger, "onfido API: retrieve live photo info error", err)
		_ = s.teller.KycAlarm(ctx, "onfido API: retrieve live photo info error", err)
		return nil, err
	}
	if apiErr := s.apiErrorHandler(ctx, path, httpCode, respBody); apiErr != nil {
		return nil, apiErr
	}
	logger.Info("onfido API: retrieve document info response", zap.String("respBody", string(respBody)))
	if err = json.Unmarshal(respBody, resp); err != nil {
		bzap.LogError(logger, "onfido API: retrieve live photo info json unmarshal error", err)
		_ = s.teller.KycAlarm(ctx, "onfido API: retrieve live photo info json unmarshal error", err)
		return nil, err
	}
	s.logger.Info("onfido API: retrieve live photo info success", zap.Reflect("resp", resp))

	return resp, nil
}

func (s *OnfidoProvider) DownloadLivePhoto(ctx context.Context, photoID string) ([]byte, error) {
	var (
		err      error
		httpCode int
		respBody []byte
	)
	path := fmt.Sprintf(PathDownloadLivePhoto, photoID)
	reqID := helper.ReqID(ctx)
	logger := s.logger.With(zap.String("reqID", reqID), zap.String("path", path))
	httpCode, respBody, err = s.request(ctx, path, http.MethodGet, nil, config.GetDownloadImageTimeout())
	if err != nil {
		bzap.LogError(logger, "onfido API: download live photo error", err)
		_ = s.teller.KycAlarm(ctx, "onfido API: download live photo error", err)
		return nil, err
	}
	if httpCode == http.StatusNotFound {
		s.logger.Info("onfido API: download live photo not found", zap.Any("path", path))
		return nil, nil
	}
	if apiErr := s.apiErrorHandler(ctx, path, httpCode, respBody); apiErr != nil {
		return nil, apiErr
	}
	s.logger.Info("onfido API: download live photo success")

	return respBody, nil
}

func (s *OnfidoProvider) getDocument(ctx context.Context, inspectionID, docID string) (*dto.Document, error) {
	var (
		err error
	)
	defer func() {
		if err != nil {
			_ = s.teller.KycAlarm(ctx, "onfido statement: download document ID:"+docID+" error", err)
		}
	}()
	logger := s.logger.With(zap.String("reqID", helper.ReqID(ctx)),
		zap.String("inspectionID", inspectionID),
		zap.String("docID", docID))
	// 下载图片
	b, err := s.DownloadDocument(ctx, docID)
	if err != nil {
		return nil, err
	}
	if b == nil {
		return nil, nil
	}

	mime := http.DetectContentType(b)
	logger.Info("content type", zap.String("mime", mime))
	if utils.IsValidMediaType(b) {
		doc := new(dto.Document)
		doc.ID = docID
		doc.Data = b // raw response
		doc.Mime = mime
		return doc, nil
	} else {
		s.logger.Warn("onfido statement: download document not image", zap.String("docID", docID), zap.String("mime", mime))
	}

	return nil, constdata.ImageTypeError
}

func (s *OnfidoProvider) getLivePhoto(ctx context.Context, inspectionID, photoID string) (*dto.Document, error) {
	var (
		err error
	)
	defer func() {
		if err != nil {
			_ = s.teller.KycAlarm(ctx, "onfido statement: download live photo ID:"+photoID+" error", err)
		}
	}()

	logger := s.logger.With(zap.String("reqID", helper.ReqID(ctx)),
		zap.String("inspectionID", inspectionID),
		zap.String("photoID", photoID))
	b, err := s.DownloadLivePhoto(ctx, photoID)
	if err != nil {
		return nil, err
	}
	if b == nil {
		return nil, nil
	}

	mime := http.DetectContentType(b)
	if utils.IsValidMediaType(b) {
		doc := new(dto.Document)
		doc.ID = photoID
		doc.Data = b // raw response
		doc.Mime = mime
		return doc, nil
	} else {
		logger.Warn("onfido statement: live photo image mime type error", zap.String("photoID", photoID), zap.String("mime", mime))
	}

	return nil, constdata.ImageTypeError
}

func (s *OnfidoProvider) GetImageRaw(ctx context.Context, actionImg *dto.KYCActionImage) (*dto.Document, error) {
	if actionImg == nil {
		return nil, nil
	}
	if actionImg.ImgType == dto.ImgTypeFace { // face
		return s.getLivePhoto(ctx, actionImg.RecordID, actionImg.ImgID)
	} else {
		return s.getDocument(ctx, actionImg.RecordID, actionImg.ImgID)
	}
}

func (s *OnfidoProvider) GetLatestLivePhotoByOldModel(ctx context.Context, applicantID string) (*LivePhotoInfo, error) {
	ctx = logx.ContextWithFields(ctx, logc.Field("func", "GetLatestLivePhotoByOldModel"), logc.Field("applicantID", applicantID))
	photos, err := s.GetListLivePhotos(ctx, applicantID)
	if err != nil {
		logc.Errorw(ctx, "onfido API: GetListLivePhotos error", logc.Field("err", err))
		return nil, err
	}
	if photos == nil || len(photos.LivePhotos) < 1 {
		logc.Infow(ctx, "onfido API: GetListLivePhotos not found")
		return nil, nil
	}
	var (
		maxCreateAt int64
		latestPhoto *LivePhotoInfo
	)
	for _, photo := range photos.LivePhotos {
		t, _ := timeutil.GetDateTZ2Unix(photo.CreatedAt)
		if t >= maxCreateAt {
			latestPhoto = photo
		}
	}
	return latestPhoto, nil
}

// GetLatestPOIDocumentsByOldModel Get latest poi documents by old model
func (s *OnfidoProvider) GetLatestPOIDocumentsByOldModel(ctx context.Context, applicantID string) ([]*DocumentInfo, error) {
	var retDocs []*DocumentInfo
	ctx = logx.ContextWithFields(ctx, logc.Field("func", "GetLatestPOIDocumentsByOldModel"), logc.Field("applicantID", applicantID))
	docs, err := s.GetListDocuments(ctx, applicantID)
	if err != nil {
		logc.Errorw(ctx, "onfido API: GetListLivePhotos error", logc.Field("err", err))
		return retDocs, err
	}
	if docs == nil || len(docs.Documents) < 1 {
		logc.Infow(ctx, "onfido API: GetListLivePhotos not found")
		return retDocs, nil
	}
	var (
		maxFrontCreateAt, maxBackCreateAt int64
		latestFrontDoc, latestBackDoc     *DocumentInfo
	)
	for _, doc := range docs.Documents {
		if doc.Type.GetDocType() < dto.Bill {
			if doc.Side == "front" {
				t, _ := timeutil.GetDateTZ2Unix(doc.CreatedAt)
				if t >= maxFrontCreateAt {
					latestFrontDoc = doc
				}
			} else if doc.Side == "back" {
				t, _ := timeutil.GetDateTZ2Unix(doc.CreatedAt)
				if t >= maxBackCreateAt {
					latestBackDoc = doc
				}
			}
		}
	}
	retDocs = append(retDocs, latestFrontDoc, latestBackDoc)
	return retDocs, nil
}

// GetListDocuments Get list documents
func (s *OnfidoProvider) GetListDocuments(ctx context.Context, applicantID string) (*ListDocuments, error) {
	var (
		err      error
		httpCode int
		respBody []byte
	)
	path := fmt.Sprintf(PathListDocuments, applicantID)
	reqID := helper.ReqID(ctx)
	ctx = logx.ContextWithFields(ctx, logc.Field("reqID", reqID), logc.Field("applicantID", applicantID), logc.Field("path", path))
	resp := new(ListDocuments)
	httpCode, respBody, err = s.request(ctx, path, http.MethodGet, nil, defaultTimeout)
	if err != nil {
		logc.Errorw(ctx, "onfido API: GetListDocuments error", logc.Field("err", err))
		_ = s.teller.KycAlarm(ctx, "onfido API: GetListDocuments error", err)
		return nil, err
	}
	if httpCode == http.StatusNotFound {
		s.logger.Info("onfido API: GetListDocuments not found", zap.Any("path", path))
		return nil, nil
	}
	if apiErr := s.apiErrorHandler(ctx, path, httpCode, respBody); apiErr != nil {
		return nil, apiErr
	}
	logc.Infow(ctx, "onfido API: GetListDocuments success", logc.Field("respBody", string(respBody)))
	if err = json.Unmarshal(respBody, resp); err != nil {
		logc.Errorw(ctx, "onfido API: GetListDocuments json unmarshal error", logc.Field("err", err))
		_ = s.teller.KycAlarm(ctx, "onfido API: GetListDocuments json unmarshal error", err)
		return nil, err
	}
	logc.Infow(ctx, "onfido API: GetListDocuments info success", logc.Field("resp", resp))

	return resp, nil
}

// GetListLivePhotos Get list live photos
func (s *OnfidoProvider) GetListLivePhotos(ctx context.Context, applicantID string) (*ListLivePhotos, error) {
	var (
		err      error
		httpCode int
		respBody []byte
	)
	path := fmt.Sprintf(PathListLivePhotos, applicantID)
	reqID := helper.ReqID(ctx)
	ctx = logx.ContextWithFields(ctx, logc.Field("reqID", reqID), logc.Field("applicantID", applicantID), logc.Field("path", path))
	resp := new(ListLivePhotos)
	httpCode, respBody, err = s.request(ctx, path, http.MethodGet, nil, defaultTimeout)
	if err != nil {
		logc.Errorw(ctx, "onfido API: GetListLivePhotos error", logc.Field("err", err))
		_ = s.teller.KycAlarm(ctx, "onfido API: GetListLivePhotos error", err)
		return nil, err
	}
	if httpCode == http.StatusNotFound {
		s.logger.Info("onfido API: GetListLivePhotos not found", zap.Any("path", path))
		return nil, nil
	}
	if apiErr := s.apiErrorHandler(ctx, path, httpCode, respBody); apiErr != nil {
		return nil, apiErr
	}
	logc.Infow(ctx, "onfido API: GetListLivePhotos success", logc.Field("respBody", string(respBody)))
	if err = json.Unmarshal(respBody, resp); err != nil {
		logc.Errorw(ctx, "onfido API: GetListLivePhotos json unmarshal error", logc.Field("err", err))
		_ = s.teller.KycAlarm(ctx, "onfido API: GetListLivePhotos json unmarshal error", err)
		return nil, err
	}
	logc.Infow(ctx, "onfido API: GetListLivePhotos info success", logc.Field("resp", resp))

	return resp, nil
}

func uploadDocument(ctx context.Context, logger *zap.Logger, hashID string, ch <-chan *dto.Document) (map[string]*dto.Document, error) {
	var (
		secret = bsecretsecret.Secret([]byte(hashID))
		bucket = byoneconfig.GetAWSS3Config().Bucket
	)

	sess, err := helper.GetAWSS3Session()
	if err != nil {
		return nil, err
	}

	uploader := s3manager.NewUploader(sess)
	docs := make(map[string]*dto.Document)

	for doc := range ch {
		logger.Info("onfido statement: upload file:" + doc.Tag + " to s3 start")
		doc.Key = fmt.Sprintf("%s/%s", hashID, doc.ID)
		doc.Bucket = bucket
		docs[doc.ID] = doc
		body, e := secret.Seal(doc.Data)
		if e != nil {
			return nil, err
		}
		f := helper.TimeCost(logger, "UploadWithContext")
		_, err := uploader.UploadWithContext(ctx, &s3manager.UploadInput{
			Bucket: aws.String(bucket),
			Key:    aws.String(doc.Key),
			Body:   bytes.NewReader(body),
		})
		f()
		if err != nil {
			logger.Error("onfido statement: upload file-"+doc.Tag+" to s3 failed", zap.Error(err))
			return nil, err
		}
		logger.Info("onfido statement: upload file-" + doc.Tag + " to s3 success")
	}

	return docs, nil
}

func (s *OnfidoProvider) GetKYCOnboardingPOIActionImages(ctx context.Context, memberID int64, accountID, recordID, idempotentID string) (actionImgs []*dto.KYCActionImage, err error) {
	detail, errRun := s.RetrieveWorkflowRun(ctx, recordID)
	if errRun != nil {
		err = errRun
		s.logger.Error("onfido state: retrieve workflow run error", zap.Error(errRun), zap.String("recordID", recordID))
		return
	}
	output, errOut := OnfidoWorkflowOutputCov(detail.Output, detail.Level)
	if errOut != nil {
		err = errOut
		s.logger.Error("onfido state: ensureWorkflowOutput error", zap.Error(errOut), zap.Any("output", detail.Output))
		return
	}

	actionImgs, _ = s.GetActionImages(ctx, memberID, 1, accountID, idempotentID, recordID, output)

	return
}
